Skip to content

fix(sendingpolicy): lock the agent FOR NO KEY UPDATE in the accept path - #1006

Merged
jiashuoz merged 3 commits into
mainfrom
fix/sending-gate-lock-order
Sep 5, 2026
Merged

fix(sendingpolicy): lock the agent FOR NO KEY UPDATE in the accept path#1006
jiashuoz merged 3 commits into
mainfrom
fix/sending-gate-lock-order

Conversation

@jiashuoz

@jiashuoz jiashuoz commented Sep 5, 2026

Copy link
Copy Markdown
Member

The v1.9.0 staging release pipeline failed its conformance gate on 03-concurrency: 8 parallel sends from HITL agent with HTTP 500 failed to hold message for approval. Staging Postgres logs show deadlock detected (SQLSTATE 40P01):

Process A: INSERT INTO messages ...   (waiting in the account_usage storage trigger)
Process B: SELECT user_id FROM agent_identities WHERE id = $1 FOR UPDATE

Cause. Each accept transaction inserts the message first. The insert takes FOR KEY SHARE on the agent row (foreign key) and a row lock on account_usage (storage trigger). It then prepares the sending operation, which locked the agent FOR UPDATE. FOR UPDATE conflicts with KEY SHARE, so with two concurrent sends A waits for B's key share while B waits for A's account_usage row.

The direct-send path (PrepareExternalTx, B6) has the identical shape. Staging has no parallel direct-send test, so it passed by omission; the new API-level test reproduces the deadlock there too.

Fix. FOR NO KEY UPDATE on the agent (and webhook) row in PrepareExternalTx / PrepareNotificationTx. It still serializes gate callers against each other and against any update or delete of the row, which is all the ordering the design needs, and it does not conflict with a foreign-key share.

Tests. TestPrepareDoesNotDeadlockAgainstConcurrentInsert (sendingpolicy, deterministic two-transaction interleaving, both prepare paths) and TestParallelSendsFromOneAgentAllAccept (e2e, eight parallel API sends). Both fail with FOR UPDATE restored and pass with the fix.

Blocks the v1.9.0 promotion; the release will be re-cut as v1.9.1 on this fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AjfGxvXW6fNKWGFHuo68yX

jiashuoz and others added 3 commits September 5, 2026 12:48
The v1.9.0 staging conformance gate failed on eight parallel HITL holds:
SQLSTATE 40P01. Each accept transaction inserts its message first, which
takes a FOR KEY SHARE lock on the agent row through the foreign key and a
row lock on account_usage through the storage trigger, then prepares its
operation, which locked the agent FOR UPDATE. FOR UPDATE conflicts with
KEY SHARE, so two concurrent sends waited on each other. The direct send
path (PrepareExternalTx) has the identical shape and deadlocks the same
way under parallel sends; staging simply never ran that case.

FOR NO KEY UPDATE keeps every ordering the gate needs (callers serialize
against each other and against any update or delete of the row) and does
not conflict with a foreign-key share. Same change for the webhook row.

Two regression tests reproduce the deadlock deterministically at the
gate and through the API, and both fail with the old lock.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjfGxvXW6fNKWGFHuo68yX
…g it

Review of the lock-order fix: the gate test's 300ms sleep could let A
finish before B ever blocked, passing vacuously against the bug. B now
reports its backend pid and A waits until pg_stat_activity shows it
blocked on a lock. The e2e test no longer calls t.Fatal from worker
goroutines, and the PrepareExternalTx ordering comment now describes the
function rather than every caller.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjfGxvXW6fNKWGFHuo68yX
The staging gate only sent in parallel from a HITL agent; the direct
accept path has the same insert-then-lock shape and carries almost all
traffic. Add the eight-parallel-direct-sends case, write the accept
transaction's lock order into the pipeline design doc, and record the
FOR KEY SHARE / FOR UPDATE rule in AGENTS.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjfGxvXW6fNKWGFHuo68yX
@jiashuoz
jiashuoz merged commit ce29e5b into main Sep 5, 2026
29 checks passed
@jiashuoz
jiashuoz deleted the fix/sending-gate-lock-order branch September 5, 2026 21:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant